﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SlimMath;
using System.Collections;

/**
 * (*1) 
 * 汎用のMatrix要素はあまり使われないのでメモリの節約のため遅延アロケートで実装している
 * */
/**
 * (*2)
 * SlimMathはDirectX系のライブラリなので本来計算で使われる変換行列の転置を渡さなければならない。
 * 誰かOpenGL系のSlimMathライブラリを作ってくれないだろうか...
 * */


namespace M3G {

    public abstract class Transformable : Object3D {
        #region Field
        float[]  translation;
        float[]  orientation;
        float[]  scale;
        float[]  matrix;
        #endregion

        #region Method
        public Transformable () {
            this.translation = new float[] { 0, 0, 0 };
            this.orientation = new float[] { 0, 0, 0, 1 };
            this.scale = new float[] { 1, 1, 1 };
            this.matrix = null;   // (*1)
        }

        protected Transformable (Transformable transformable)
            : base (transformable) {
            this.translation = transformable.translation;
            this.orientation = transformable.orientation;
            this.scale = transformable.scale;
            this.matrix = (transformable.matrix == null) ? null : (float[])transformable.matrix.Clone ();
        }


        public float[] Translation {
            get {
                return (float[])translation.Clone ();
            }
        }


        public void SetTranslation (float tx, float ty, float tz) {
            this.translation[0] = tx;
            this.translation[1] = ty;
            this.translation[2] = tz;
        }

        public float[] Orientation {
            get {
                var q = new Quaternion (orientation);
                var angle = q.Angle * 180 / (float)Math.PI;
                var axis  = q.Axis;
                axis.Normalize ();

                return new float[] { angle, axis[0], axis[1], axis[2] };
            }
        }

        public float[] OrientationQuat {
            get {
                return (float[])orientation.Clone ();
            }
        }


        public void SetOrientation (float angle, float ax, float ay, float az) {
            if (angle != 0 && ax == 0 && ay == 0 && az == 0) {
                throw new ArgumentException ("Rotation around axis(0,0,0) is invalid");
            }

            var q = Quaternion.RotationAxis (new Vector3 (ax, ay, az), angle * (float)Math.PI / 180.0f);
            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }

        /**
         *
         * */
        public void SetOrientationLookAt (float targetX, float targetY, float targetZ, float upX, float upY, float upZ) {
            var target  = new Vector3 (targetX, targetY, targetZ);
            var up      = new Vector3 (upX, upY, upZ);

            var x = Vector3.Cross (target, up);
            var y = Vector3.Cross (x, target);
            var z = -target;

            if (target.LengthSquared == 0 || up.LengthSquared == 0) {
                throw new ArgumentException ("Target or Up vector is zero");
            }
            if (x.LengthSquared < Mathf.Epsilon) {
                throw new ArgumentException ("Target and Up vector is parallel");
            }

            x.Normalize ();
            y.Normalize ();
            z.Normalize ();

            // (*2) 
            var rot = new float[16]{x[0], x[1], x[2], 0,
                                    y[0], y[1], y[2], 0,
                                    z[0], z[1], z[2], 0,
                                    0, 0, 0, 1};

            var q = Quaternion.RotationMatrix (new Matrix (rot));
            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }

        public void SetOrientationQuat (float qx, float qy, float qz, float qw) {
            if (qx == 0 && qy == 0 && qz == 0 && qw == 0) {
                throw new ArgumentException ("Zero quaternion(0,0,0,0) is invalid");
            }

            this.orientation[0] = qx;
            this.orientation[1] = qy;
            this.orientation[2] = qz;
            this.orientation[3] = qw;
        }

        public float[] Scaling {
            get { return (float[])scale.Clone (); }
        }



        public void SetScale (float sx, float sy, float sz) {
            this.scale[0] = sx;
            this.scale[1] = sy;
            this.scale[2] = sz;
        }

        public Transform Transform {
            get {
                return (matrix == null) ? new Transform () : new Transform (matrix);
            }
        }

        public void SetTransform (Transform transform) {
            if (this is Node) {
                var m = transform.Matrix;
                if (!(m[12] == 0 && m[13] == 0 && m[14] == 0 && m[15] == 1)) {
                    throw new ArgumentException ("Bottom row vector must be 0,0,0,1 if this is node");
                }
            }

            this.matrix = (transform == null) ? null : transform.Matrix;
        }

        public Transform CompositeTransform {
            get {
                var t = new Transform ();
                t.PostTranslate (translation[0], translation[1], translation[2]);
                t.PostRotateQuat (orientation[0], orientation[1], orientation[2], orientation[3]);
                t.PostScale (scale[0], scale[1], scale[2]);
                if (matrix != null) {
                    t.PostMultiply (new Transform (matrix));
                }
                return t;
            }
        }


        public void PreRotate (float angle, float ax, float ay, float az) {
            if (angle != 0 && ax == 0 && ay == 0 && az == 0) {
                throw new ArgumentException ("Rotation around (0,0,0) axis is invalid");
            }

            var q1 = new Quaternion (orientation);
            var q2 = Quaternion.RotationAxis (new Vector3 (ax, ay, az), angle * (float)Math.PI / 180.0f);
            var q  = q2 * q1;

            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }

        public void PreRotateQuat (float qx, float qy, float qz, float qw) {
            if (qx == 0 && qy == 0 && qz == 0 && qw == 0) {
                throw new ArgumentException ("Zero quaternion(0,0,0,0) is invalid");
            }

            var q1 = new Quaternion (orientation);
            var q2 = new Quaternion (qx, qy, qz, qw);
            var q  = q2 * q1;

            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }


        public void PostRotate (float angle, float ax, float ay, float az) {
            if (angle != 0 && ax == 0 && ay == 0 && az == 0) {
                throw new ArgumentException ("Rotation around (0,0,0) axis is invalid");
            }

            var q1 = new Quaternion (orientation);
            var q2 = Quaternion.RotationAxis (new Vector3 (ax, ay, az), angle * (float)Math.PI / 180.0f);
            var q  = q1 * q2;

            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }

        public void PostRotateQuat (float qx, float qy, float qz, float qw) {
            if (qx == 0 && qy == 0 && qz == 0 && qw == 0) {
                throw new ArgumentException ("Zero quaternion(0,0,0,0) is invalid");
            }

            var q1 = new Quaternion (orientation);
            var q2 = new Quaternion (qx, qy, qz, qw);
            var q  = q1 * q2;

            this.orientation[0] = q.X;
            this.orientation[1] = q.Y;
            this.orientation[2] = q.Z;
            this.orientation[3] = q.W;
        }

        public void Scale (float sx, float sy, float sz) {

            this.scale[0] *= sx;
            this.scale[1] *= sy;
            this.scale[2] *= sz;
        }


        public void Translate (float tx, float ty, float tz) {

            this.translation[0] += tx;
            this.translation[1] += ty;
            this.translation[2] += tz;
        }


        public override int Animate (int time) {

            base.Animate (time);

            var animatedOrientation = new float[4];
            var animatedScale       = new float[3];
            var animatedTranslation = new float[3];
            var value               = new float[4];
            var animated            = new Dictionary<int, bool> (3);

            for (var i=0; i < AnimationTrackCount; i++) {
                var track = AnimationTrack[i];
                var ch    = AnimationTrackChannel[i];
                if (track.Controller == null || !track.Controller.Active (time)) {
                    continue;
                }
                var property = track.TargetProperty;
                var weight   = track.Controller.Weight;

                animated[property] = true;
                switch (property) {
                    case M3G.AnimationTrack.Orientation: {
                        track.Sample (time, ch, value);
                        animatedOrientation[0] += value[0] * weight;
                        animatedOrientation[1] += value[1] * weight;
                        animatedOrientation[2] += value[2] * weight;
                        animatedOrientation[3] += value[3] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.Scale: {
                        track.Sample (time, ch, value);
                        if (track.KeyframeSequence.ChanelCount == 1) {
                            value[2] = value[1] = value[0];
                        }
                        animatedScale[0] += value[0] * weight;
                        animatedScale[1] += value[1] * weight;
                        animatedScale[2] += value[2] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.Translation: {
                        track.Sample (time, ch, value);
                        animatedTranslation[0] = value[0] * weight;
                        animatedTranslation[1] = value[1] * weight;
                        animatedTranslation[2] = value[2] * weight;
                        break;
                    }
                    default: {
                        break;
                    }
                } // switch

            } // for

            if (animated[M3G.AnimationTrack.Scale]) {
                var animatedQuat = new Quaternion (animatedOrientation);
                animatedQuat.Normalize ();
                this.orientation[0] = animatedQuat[0];
                this.orientation[1] = animatedQuat[1];
                this.orientation[2] = animatedQuat[2];
                this.orientation[3] = animatedQuat[3];
            }
            if (animated[M3G.AnimationTrack.Orientation]) {
                this.scale[0] = animatedScale[0];
                this.scale[1] = animatedScale[1];
                this.scale[2] = animatedScale[2];
            }
            if (animated[M3G.AnimationTrack.Translation]) {
                this.translation[0] = animatedTranslation[0];
                this.translation[1] = animatedTranslation[1];
                this.translation[2] = animatedTranslation[2];
            }
            return 0;

        }

        #endregion


    }
}


